package com.test.config;

import lombok.extern.log4j.Log4j2;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.regex.Pattern;

@Log4j2
public class PasswordEncode implements PasswordEncoder {

    private Pattern BCRYPT_PATTERN = Pattern.compile("^\\A\\$5(a|y|b)?\\$(\\d\\d)\\$.*");

    private final int BCRYPT_SALT_LEN = 32;

    private final char base64_code[] = {'.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
            'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
            'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1',
            '2', '3', '4', '5', '6', '7', '8', '9'};

    private final byte index_64[] = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            0, 1, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, -1, -1, -1, -1, -1, -1, -1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
            12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, -1, -1, -1, -1, -1, -1, 28, 29, 30, 31, 32,
            33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, -1, -1, -1, -1, -1};

    private final int strength;

    private final PasswordEncode.CustomBCryptVersion version;

    private String algorithm = "SHA-256";

//    public static void main(String[] args) {
//        PasswordEncode bcryptPasswordEncoder = new PasswordEncode();
//        String password = "123456";
//        String encode1 = bcryptPasswordEncoder.encode(password);
//        System.out.println("encode1:" + encode1);
//        boolean flag1 = bcryptPasswordEncoder.matches(password, "$5a$10$XDfUPVTyRVnycDm2XhXwRiCzXVXWXUfQKjPmKhKuZU2e8oUTx4TGURlhsmksacmS4FdOY1DyOuBqYQz0j+1j+s=");
//        System.out.println("flag1:" + flag1);
//    }

    public PasswordEncode() {

        this(PasswordEncode.CustomBCryptVersion.$5A, -1);
    }

    public PasswordEncode(PasswordEncode.CustomBCryptVersion version, int strength) {
        this.version = version;
        this.strength = (strength == -1) ? 10 : strength;
    }

    public enum CustomBCryptVersion {

        $5A("$5a"),

        $5Y("$5y"),

        $5B("$5b");

        private final String version;

        CustomBCryptVersion(String version) {
            this.version = version;
        }

        public String getVersion() {
            return this.version;
        }
    }

    @Override
    public String encode(CharSequence rawPassword) {
        if (rawPassword == null)
            throw new RuntimeException("密码不能为空！");
        try {
            String salt = getSalt();
            return hashpw(rawPassword.toString(), salt);
        } catch (Exception e) {
            System.out.println("密码加密失败" + e);
            throw new RuntimeException("密码加密失败");
        }
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (rawPassword == null)
            throw new RuntimeException("密码不能为空！");
        if (encodedPassword == null || encodedPassword.length() == 0) {
            return false;
        }
        if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
            return false;
        }
        try {
            return checkpw(rawPassword.toString(), encodedPassword);
        } catch (Exception e) {
            log.error("解密失败:{}", e.getMessage());
            return false;
        }
    }


    public String getSalt() {
        String prefix = this.version.getVersion();
        int log_rounds = this.strength;

        StringBuilder rs = new StringBuilder();
        String randomSalt = RandomStringUtils.random(BCRYPT_SALT_LEN, new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
                'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
                'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1',
                '2', '3', '4', '5', '6', '7', '8', '9'}); //32位salt
        byte[] rnd = randomSalt.getBytes(StandardCharsets.UTF_8);

        if (!prefix.startsWith("$5")
                || (prefix.charAt(2) != 'a' && prefix.charAt(2) != 'y' && prefix.charAt(2) != 'b')) {
            throw new IllegalArgumentException("Invalid prefix");
        }
        if (log_rounds < 4 || log_rounds > 31) {
            throw new IllegalArgumentException("Invalid log_rounds");
        }

        rs.append("$5");
        rs.append(prefix.charAt(2));
        rs.append("$");
        if (log_rounds < 10) {
            rs.append("0");
        }
        rs.append(log_rounds);
        rs.append("$");
        encode_base64(rnd, rnd.length, rs);
        return rs.toString();
    }

    private void encode_base64(byte d[], int len, StringBuilder rs) throws IllegalArgumentException {
        int off = 0;
        int c1, c2;

        if (len <= 0 || len > d.length) {
            throw new IllegalArgumentException("Invalid len");
        }

        while (off < len) {
            c1 = d[off++] & 0xff;
            rs.append(base64_code[(c1 >> 2) & 0x3f]);
            c1 = (c1 & 0x03) << 4;
            if (off >= len) {
                rs.append(base64_code[c1 & 0x3f]);
                break;
            }
            c2 = d[off++] & 0xff;
            c1 |= (c2 >> 4) & 0x0f;
            rs.append(base64_code[c1 & 0x3f]);
            c1 = (c2 & 0x0f) << 2;
            if (off >= len) {
                rs.append(base64_code[c1 & 0x3f]);
                break;
            }
            c2 = d[off++] & 0xff;
            c1 |= (c2 >> 6) & 0x03;
            rs.append(base64_code[c1 & 0x3f]);
            rs.append(base64_code[c2 & 0x3f]);
        }
    }

    private boolean checkpw(String plaintext, String encodedPassword) {
        String pass = hashpw(plaintext, encodedPassword);
        return equalsNoEarlyReturn(encodedPassword, pass);
    }

    private boolean equalsNoEarlyReturn(String a, String b) {
        return MessageDigest.isEqual(a.getBytes(StandardCharsets.UTF_8), b.getBytes(StandardCharsets.UTF_8));
    }

    private String hashpw(String password, String salt) {
        char minor = (char) 0;
        int rounds, off;
        StringBuilder rs = new StringBuilder();

        if (salt == null) {
            throw new IllegalArgumentException("salt cannot be null");
        }

        int saltLength = salt.length();

        if (saltLength < 28) {
            throw new IllegalArgumentException("Invalid salt");
        }

        if (salt.charAt(0) != '$' || salt.charAt(1) != '5') {
            throw new IllegalArgumentException("Invalid salt version");
        }
        if (salt.charAt(2) == '$') {
            off = 3;
        } else {
            minor = salt.charAt(2);
            if ((minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b') || salt.charAt(3) != '$') {
                throw new IllegalArgumentException("Invalid salt revision");
            }
            off = 4;
        }

        // Extract number of rounds
        if (salt.charAt(off + 2) > '$') {
            throw new IllegalArgumentException("Missing salt rounds");
        }

        if (off == 4 && saltLength < 29) {
            throw new IllegalArgumentException("Invalid salt");
        }
        rounds = Integer.parseInt(salt.substring(off, off + 2));
        String real_salt = salt.substring(off + 3, off + 3 + 43); //32位salt经过encode_base64变为长度43字符，所以截取的时候是43长度
        String s = StringUtils.substringBetween(password, "[", "]"); //混淆
        if (!StringUtils.isEmpty(s)) {
            s = stringMD5(s);
            real_salt += s;
        }
        String encodePass = null;
        try {
            MessageDigest md = MessageDigest.getInstance(algorithm);
            byte[] passBytes = mergePasswordAndSalt(password, real_salt).getBytes(StandardCharsets.UTF_8);
            encodePass = new String(Base64.encodeBase64(md.digest(passBytes)));
        } catch (Exception e) {
            System.out.println("密码加密失败" + e);
            throw new RuntimeException("密码加密失败！");
        }

        rs.append("$5");
        if (minor >= 'a') {
            rs.append(minor);
        }
        rs.append("$");
        if (rounds < 10) {
            rs.append("0");
        }
        rs.append(rounds);
        rs.append("$");
        rs.append(real_salt);
        rs.append(encodePass);
        return rs.toString();
    }

    /**
     * @param password 密码
     * @return 合并密码
     */
    private String mergePasswordAndSalt(String password, String salt) {
        if (password == null) {
            password = "";
        }
        return password + "{" + salt + "}";
    }

    public static String stringMD5(String str) {
        String hexStr = "";
        try {
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            byte[] digest = md5.digest(str.getBytes("utf-8"));
            hexStr = byteArrayToHex(digest);
        } catch (Exception e) {
            System.out.println("获取md5值失败" + e);
            throw new RuntimeException("获取md5值失败");
        }
        return hexStr;
    }

    static String byteArrayToHex(byte[] byteArray) {
        // 首先初始化一个字符数组，用来存放每个16进制字符
        char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
        // new一个字符数组，这个就是用来组成结果字符串的（解释一下：一个byte是八位二进制，也就是2位十六进制字符（2的8次方等于16的2次方））
        char[] resultCharArray = new char[byteArray.length * 2];
        // 遍历字节数组，通过位运算（位运算效率高），转换成字符放到字符数组中去
        int index = 0;
        for (byte b : byteArray) {
            resultCharArray[index++] = hexDigits[b >>> 4 & 0xf];
            resultCharArray[index++] = hexDigits[b & 0xf];
        }
        // 字符数组组合成字符串返回
        return new String(resultCharArray);
    }

}
